/*
 * Copyright (C) 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.smartandroid.sa.json.internal.bind;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.smartandroid.sa.json.reflect.TypeToken;
import com.smartandroid.sa.json.stream.JsonReader;
import com.smartandroid.sa.json.stream.JsonWriter;

/**
 * A basic binding between JSON and Java objects.
 */
public final class MiniGson {
	/**
	 * This thread local guards against reentrant calls to getAdapter(). In
	 * certain object graphs, creating an adapter for a type may recursively
	 * require an adapter for the same type! Without intervention, the recursive
	 * lookup would stack overflow. We cheat by returning a proxy type adapter.
	 * The proxy is wired up once the initial adapter has been created.
	 */
	private final ThreadLocal<Map<TypeToken<?>, FutureTypeAdapter<?>>> calls = new ThreadLocal<Map<TypeToken<?>, FutureTypeAdapter<?>>>() {
		@Override
		protected Map<TypeToken<?>, FutureTypeAdapter<?>> initialValue() {
			return new HashMap<TypeToken<?>, FutureTypeAdapter<?>>();
		}
	};

	private final List<TypeAdapter.Factory> factories;

	private MiniGson(Builder builder) {
		List<TypeAdapter.Factory> factories = new ArrayList<TypeAdapter.Factory>();
		if (builder.addDefaultFactories) {
			factories.add(TypeAdapters.BOOLEAN_FACTORY);
			factories.add(TypeAdapters.INTEGER_FACTORY);
			factories.add(TypeAdapters.DOUBLE_FACTORY);
			factories.add(TypeAdapters.FLOAT_FACTORY);
			factories.add(TypeAdapters.LONG_FACTORY);
			factories.add(TypeAdapters.STRING_FACTORY);
		}
		factories.addAll(builder.factories);
		if (builder.addDefaultFactories) {
			factories.add(CollectionTypeAdapter.FACTORY);
			factories.add(StringToValueMapTypeAdapter.FACTORY);
			factories.add(ArrayTypeAdapter.FACTORY);
			factories.add(ReflectiveTypeAdapter.FACTORY);
		}
		this.factories = Collections.unmodifiableList(factories);
	}

	/**
	 * Returns the type adapter for {@code} type.
	 * 
	 * @throws IllegalArgumentException
	 *             if this GSON cannot serialize and deserialize {@code type}.
	 */
	public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
		// TODO: create a cache!

		Map<TypeToken<?>, FutureTypeAdapter<?>> threadCalls = calls.get();
		@SuppressWarnings("unchecked")
		// the key and value type parameters always agree
		FutureTypeAdapter<T> ongoingCall = (FutureTypeAdapter<T>) threadCalls
				.get(type);
		if (ongoingCall != null) {
			return ongoingCall;
		}

		FutureTypeAdapter<T> call = new FutureTypeAdapter<T>();
		threadCalls.put(type, call);
		try {
			for (TypeAdapter.Factory factory : factories) {
				TypeAdapter<T> candidate = factory.create(this, type);
				if (candidate != null) {
					call.setDelegate(candidate);
					return candidate;
				}
			}
			throw new IllegalArgumentException(
					"This MiniGSON cannot serialize " + type);
		} finally {
			threadCalls.remove(type);
		}
	}

	static class FutureTypeAdapter<T> extends TypeAdapter<T> {
		private TypeAdapter<T> delegate;

		public void setDelegate(TypeAdapter<T> typeAdapter) {
			if (delegate != null) {
				throw new AssertionError();
			}
			delegate = typeAdapter;
		}

		@Override
		public T read(JsonReader reader) throws IOException {
			if (delegate == null) {
				throw new IllegalStateException();
			}
			return delegate.read(reader);
		}

		@Override
		public void write(JsonWriter writer, T value) throws IOException {
			if (delegate == null) {
				throw new IllegalStateException();
			}
			delegate.write(writer, value);
		}
	}

	/**
	 * Returns the type adapter for {@code} type.
	 * 
	 * @throws IllegalArgumentException
	 *             if this GSON cannot serialize and deserialize {@code type}.
	 */
	public <T> TypeAdapter<T> getAdapter(Class<T> type) {
		return getAdapter(TypeToken.get(type));
	}

	/**
	 * Returns the type adapters of this context in order of precedence.
	 */
	public List<TypeAdapter.Factory> getFactories() {
		return factories;
	}

	public static final class Builder {
		private final List<TypeAdapter.Factory> factories = new ArrayList<TypeAdapter.Factory>();
		boolean addDefaultFactories = true;

		public Builder factory(TypeAdapter.Factory factory) {
			factories.add(factory);
			return this;
		}

		public Builder withoutDefaultFactories() {
			this.addDefaultFactories = false;
			return this;
		}

		public <T> Builder typeAdapter(final Class<T> type,
				final TypeAdapter<T> typeAdapter) {
			factories.add(TypeAdapters.newFactory(type, typeAdapter));
			return this;
		}

		public <T> Builder typeAdapter(TypeToken<T> type,
				TypeAdapter<T> typeAdapter) {
			factories.add(TypeAdapters.newFactory(type, typeAdapter));
			return this;
		}

		public <T> Builder typeHierarchyAdapter(TypeToken<T> type,
				TypeAdapter<T> typeAdapter) {
			factories.add(TypeAdapters.newTypeHierarchyFactory(type,
					typeAdapter));
			return this;
		}

		public MiniGson build() {
			return new MiniGson(this);
		}
	}
}
